package code.google.antanno;

import java.io.*;
import java.util.*;

import org.apache.tools.ant.types.Parameter;

import org.objectweb.asm.ClassReader;

public class AnnotationSelector extends
    org.apache.tools.ant.types.selectors.BaseExtendSelector {

    private List<String> values     = new ArrayList<String>();

    private String annotation       = null;
    private EXISTS exists           = null;
    private EQUALS equals           = null;
    private Condition test          = null;
    private boolean classesOnly     = false;

    public boolean isSelected(File basedir, String filename, File file) {
        validate();

        if (!filename.endsWith(".class")) return !classesOnly;

        String name = file.getName();
        final int $ = name.indexOf('$');
        if ($ > 0) {
            file = new File(file.getParent(), name.substring(0, $)+".class");
            if (!(file.isFile() && file.exists())) return false;
        }

        InputStream is = null;
        try {
            is = new BufferedInputStream(new FileInputStream(file));
            CheckAnnotation ca = new CheckAnnotation();
            ca.setTargetAnnotation(annotation);
            ClassReader cr = new ClassReader(is);
            cr.accept(ca, 0);
            if (exists != null) exists.setValue(ca.isExists());
            if (equals != null)
                for (String s : ca.getValue()) equals.addValue(s);
        } catch (Throwable t) {
            t.printStackTrace();
            return false;
        } finally {
            if (is != null) try { is.close(); } catch (Throwable t) {};
        }

        return test.check();
    }

    public void setParameters(Parameter[] p) {
        super.setParameters(parameters);
        exists = null;
        equals = null;
        test   = null;
        values.clear();
        annotation = null;

        for (int i = 0, count = (p != null ? p.length : -1); i < count; i++) {
            String name  = p[i].getName();
            String value = p[i].getValue();
            if (value == null || value.trim().length() == 0) {
                setError("Invalid parameter " + name);
                continue;
            }
            String lname = name.toLowerCase();

            if ("annotation".equals(lname)) {
                setAnnotation(value);
            } else 
            if ("value".equals(lname)) {
                setValues(value);
            } else 
            if ("condition".equals(lname)) {
                setCondition(value);
            } else 
            if ("classesonly".equalsIgnoreCase(lname) || "classes-only".equalsIgnoreCase(lname)) {
                classesOnly = "true".equalsIgnoreCase(value);
            } else {
                setError("Invalid parameter " + name);
            }
        }
        if (equals != null) equals.setList(values);
    }

    public void verifySettings() {
        if (annotation == null) {
            setError("The 'annotation' parameter is required");
        }
        if (test == null) {
            setError("The 'condition' parameter is required");
        }
        if (exists == null && equals == null) {
            setError("You must use at least one of the 'equals' or "
                    +"the 'exists' conditions");
        }
    }

    public void setAnnotation(String className) {
        annotation = "L" + className.replace('.', '/') + ";";
    }

    public void setCondition(String c) {
        String operation = null;
        boolean not = false;
        Condition[] conditions = new Condition[2];
        int count = 0;

        StringTokenizer st = new StringTokenizer(c, " \t()", false);
        while (st.hasMoreTokens()) {
            String s = st.nextToken().toLowerCase();
            if ("and".equals(s) || "or".equals(s)) {
                if (operation != null) {
                    setError(String.format("Invalid condition '%s': syntax error on token '%s'", c, s));
                    return;
                }
                operation = s;
            } else
            if ("not".equals(s)) {
                not = !not;
            } else {
                if ("exists".equals(s)) {
                    if (exists != null) {
                        setError(String.format("Invalid condition '%s': duplicate token '%s'", c, s));
                        return;
                    }
                    exists = new EXISTS();
                    conditions[count] = not ? new NOT(exists) : exists;
                    not = false;
                } else 
                if ("equals".equals(s)) {
                    if (equals != null) {
                        setError(String.format("Invalid condition '%s': duplicate token '%s'", c, s));
                        return;
                    }
                    equals = new EQUALS();
                    conditions[count] = not ? new NOT(equals) : equals;
                    not = false;
                } else {
                    setError(String.format("Invalid condition '%s': unknown token '%s'", c, s));
                    return;
                }
                count++;
                if (count >= conditions.length) break;
            }
        }

        if (count < 2 && operation != null) {
            setError(String.format("Invalid condition '%s': '%s' expected after '%s'", c, (exists != null ? "equals" : "exists"), operation));
            return;
        }
        if (count > 1 && operation == null) {
            setError(String.format("Invalid condition '%s': 'and' or 'or' expected after '%s'", c, conditions[0].toString() ));
            return;
        }
        if (st.hasMoreTokens()) {
            setError(String.format("Invalid condition '%s': unexpected token '%s'", c, st.nextToken()));
            return;
        }
        
        if (operation == null)
            test = conditions[0];
        else
            test = "and".equals(operation) 
                ? new AND(conditions[0], conditions[1]) 
                : new  OR(conditions[0], conditions[1]);
    }

    public void setValues(String list) {
        StringTokenizer st = new StringTokenizer(list, "'", false);
        while (st.hasMoreTokens()) {
            String s = st.nextToken();
            if (s.matches("[ ,;]+")) continue;
            values.add(s);
        }
    }

    private static interface Condition {
        public boolean check();
    }

    private static class NOT implements Condition {
        private Condition impl;
        public NOT(Condition c) {
            impl = c;
        }
        public boolean check() {
            return !impl.check();
        }
        public String toString() {
            return "not("+impl+")";
        }
    }

    private static class OR implements Condition {
        private Condition impl1;
        private Condition impl2;
        public OR(Condition c1, Condition c2) {
            impl1 = c1;
            impl2 = c2;
        }
        public boolean check() {
            return impl1.check() || impl2.check();
        }
        public String toString() {
            return "("+impl1+" or "+impl2+")";
        }
    }

    private static class AND implements Condition {
        private Condition impl1;
        private Condition impl2;
        public AND(Condition c1, Condition c2) {
            impl1 = c1;
            impl2 = c2;
        }
        public boolean check() {
            return impl1.check() && impl2.check();
        }
        public String toString() {
            return "("+impl1+" and "+impl2+")";
        }
    }


    private static class EXISTS implements Condition {
        private boolean value;
        public void setValue(boolean v) {
            value = v;
        }
        public boolean check() {
            return value;
        }
        public String toString() {
            return "EXISTS";
        }
    }

    private static class EQUALS implements Condition {
        private boolean value;
        private Set<String> allowed = new HashSet<String>();
        public void addValue(String s) {
            if (!value) value = allowed.contains(s);
        }
        public void setList(List<String> values) {
            allowed.addAll(values);
        }
        public boolean check() {
            return value;
        }
        public String toString() {
            return "EQUALS";
        }
    }

}